package prototype;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class NetworkLoaderController {

    private static HashMap<Integer, DigitalElement> digitalElementList = new HashMap<Integer, DigitalElement>();
    private static HashMap<String, CompositRecipe> compositRecipes = new HashMap<String, CompositRecipe>();

    private static void NormalizeFile(String fileName) throws Exception {
        //get the needed objects set up for reading and writing
        File file = new File(fileName);
        File f = new File("temp.xml");
        FileOutputStream fop = new FileOutputStream(f);
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        DataInputStream dis = null;

        fis = new FileInputStream(file);

        // Here BufferedInputStream is added for fast reading.
        bis = new BufferedInputStream(fis);
        dis = new DataInputStream(bis);

        // dis.available() returns 0 if the file does not have more lines.
        while (dis.available() != 0) {

            // this statement reads the line from the file and print it to
            // the console.
            fop.write(dis.readLine().replaceAll("\n", "").replaceAll("\t", "").getBytes());
        }

        // dispose all the resources after using them.
        fis.close();
        bis.close();
        dis.close();
        fop.close();
    }

    public static void loadNetworkFromXMLFile(String fileName) throws Exception {
        if (!digitalElementList.isEmpty()) {
            throw new Exception();
        }
        NormalizeFile(fileName);
        //check the document vs xsd
        //get a schema factory
        SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");

        //load the file
        File schemaLocation = new File("validator.xsd");

        //compile the schema
        Schema schema = factory.newSchema(schemaLocation);

        //create a validator
        Validator validator = schema.newValidator();

        //load the document to check
        Source source = new StreamSource("temp.xml");

        //check the document
        validator.validate(source);
        //System.out.println(fileName + " is valid against: validator.xsd");

        //if we get to this point the document is valid so we can start parsing
        //prepare the document
        //create a factory
        DocumentBuilderFactory Docfactory = DocumentBuilderFactory.newInstance();

        //create a builder
        DocumentBuilder builder = Docfactory.newDocumentBuilder();

        //create document
        Document doc = builder.parse("temp.xml");

        //parse header
        Node root = doc.getFirstChild();

        //Header
        Node header = root.getFirstChild();

        //if it is a header than parse it
        if (header.getNodeName().equals("Header")) {

            //get the gatelist
            NodeList compositeGates = header.getChildNodes();

            //take each composit recipe
            for (int i = 0; i < compositeGates.getLength(); i++) {
                Node oneComposit = compositeGates.item(i);

                //create composit recipe class
                CompositRecipe recipe = new CompositRecipe();

                //get the name
                String name = oneComposit.getAttributes().item(0).getNodeValue();

                //get the main node
                //body
                Node compBody = oneComposit.getFirstChild();

                //connections
                Node compConnections = compBody.getNextSibling();

                //portmap
                Node compPortmap = compConnections.getNextSibling();

                //process body
                NodeList gates = compBody.getChildNodes();
                for (int j = 0; j < gates.getLength(); j++) {
                    Node oneGate = gates.item(j);
                    //get the id
                    Integer id = Integer.parseInt(oneGate.getAttributes().item(0).getNodeValue());

                    //get the type
                    ElementType type = null;

                    if (oneGate.getNodeName().equals("Gate")) {
                        //get the type from attribute field
                        type = ElementType.valueOf(oneGate.getAttributes().getNamedItem("type").getNodeValue());
                    } else if (oneGate.getNodeName().equals("Generator")) {
                        type = ElementType.Generator;
                    } else if (oneGate.getNodeName().equals("Switch")) {
                        type = ElementType.Switch;
                    } else //Composit
                    {
                        type = ElementType.Composit;
                    }

                    //get the extra info: pattern if generator, type if composit
                    if (type.equals(ElementType.Generator)) {
                        //put the pattern info in extrainfo
                        recipe.gateListExtraInfo.put(id, oneGate.getAttributes().getNamedItem("pattern").getNodeValue());
                    } else if (type.equals(ElementType.Composit)) {
                        recipe.gateListExtraInfo.put(id, oneGate.getAttributes().getNamedItem("type").getNodeValue());
                    } else {
                        recipe.gateListExtraInfo.put(id, null);
                    }

                    //add the element to the recipe
                    recipe.gateList.put(id, type);
                }

                //process connections
                NodeList connections = compConnections.getChildNodes();
                for (int j = 0; j < connections.getLength(); j++) {
                    Node oneConnection = connections.item(j);

                    //create the array
                    Integer[] ports = new Integer[4];

                    //parse the xml
                    ports[0] = Integer.parseInt(oneConnection.getAttributes().getNamedItem("out").getNodeValue());
                    ports[1] = Integer.parseInt(oneConnection.getAttributes().getNamedItem("outPort").getNodeValue());
                    ports[2] = Integer.parseInt(oneConnection.getAttributes().getNamedItem("in").getNodeValue());
                    ports[3] = Integer.parseInt(oneConnection.getAttributes().getNamedItem("inPort").getNodeValue());

                    //add the connection to the recipe
                    recipe.connections.add(ports);
                }

                //process the portmap
                NodeList portmaps = compPortmap.getChildNodes();
                for (int j = 0; j < portmaps.getLength(); j++) {
                    Node onePortmap = portmaps.item(j);

                    //decide if it is inmap our outmap
                    if (onePortmap.getNodeName().equals("inmap")) {
                        //get the outport
                        Integer outPort = Integer.parseInt(onePortmap.getAttributes().getNamedItem("outPort").getNodeValue());

                        Integer[] ins = new Integer[2];

                        //get the input gate id and its port id
                        ins[0] = Integer.parseInt(onePortmap.getAttributes().getNamedItem("in").getNodeValue());
                        ins[1] = Integer.parseInt(onePortmap.getAttributes().getNamedItem("inPort").getNodeValue());

                        //add these to the inMapping using outPort as the key
                        //check if this port is already in use
                        if (recipe.inMapping.containsKey(outPort)) {
                            //if yes then add this to it
                            recipe.inMapping.get(outPort).add(ins);
                        } else {
                            //create the arraylist
                            ArrayList<Integer[]> b = new ArrayList<Integer[]>();

                            //add the mappings to the array
                            b.add(ins);

                            //add both to inmapping using outport as key
                            recipe.inMapping.put(outPort, b);
                        }
                    } else {
                        //get the inport
                        Integer inPort = Integer.parseInt(onePortmap.getAttributes().getNamedItem("inPort").getNodeValue());

                        Integer[] outs = new Integer[2];

                        //get out gate id and its port
                        outs[0] = Integer.parseInt(onePortmap.getAttributes().getNamedItem("out").getNodeValue());
                        outs[1] = Integer.parseInt(onePortmap.getAttributes().getNamedItem("outPort").getNodeValue());

                        //add these to the inMapping using inPort as the key
                        //note that this port can only be used once
                        if (recipe.outMapping.containsKey(inPort)) {
                            throw new Exception("Rossz portmapping");
                        }
                        recipe.outMapping.put(inPort, outs);
                    }

                }
                //finally add the recipe to the recipelist
                compositRecipes.put(name, recipe);
            }
        }

        //body
        Node body;
        if (header.getNodeName().equals("Body"))
        {
            body = header;
        } else
        {
             body = header.getNextSibling();
        }

        //this is where we build the network
        NodeList gates = body.getChildNodes();
        for (int i = 0; i < gates.getLength(); i++) {
            Node oneGate = gates.item(i);

            //get the id
            String id = oneGate.getAttributes().getNamedItem("id").getNodeValue();

            //what kind of element
            if (oneGate.getNodeName().equals("Gate")) {
                //get the type from attribute field
                ElementType type = ElementType.valueOf(oneGate.getAttributes().getNamedItem("type").getNodeValue());

                //create the element
                createDigitalElement(id, type, null);
            } else if (oneGate.getNodeName().equals("Generator")) {
                //get the pattern from attribute
                String pattern = oneGate.getAttributes().getNamedItem("pattern").getNodeValue();

                //create the element
                createDigitalElement(id, ElementType.Generator, pattern);
            } else if (oneGate.getNodeName().equals("Switch")) {
                createDigitalElement(id, ElementType.Switch, null);
            } else //Composit
            {
                //get the composit recipe name
                String pattern = oneGate.getAttributes().getNamedItem("type").getNodeValue();

                //create the composit element
                createDigitalElement(id, ElementType.Composit, pattern);
            }
        }

        //connections
        Node connections = body.getNextSibling();

        //this is where we make the connections
        NodeList wires = connections.getChildNodes();
        for (int i = 0; i < wires.getLength(); i++) {
            Node oneWire = wires.item(i);

            //get the attributes
            int out = Integer.parseInt(oneWire.getAttributes().getNamedItem("out").getNodeValue());
            int outPort = Integer.parseInt(oneWire.getAttributes().getNamedItem("outPort").getNodeValue());
            int in = Integer.parseInt(oneWire.getAttributes().getNamedItem("in").getNodeValue());
            int inPort = Integer.parseInt(oneWire.getAttributes().getNamedItem("inPort").getNodeValue());

            //create the connection
            createConnection(out, outPort, in, inPort);
        }

        //file törlése
        File f = new File("temp.xml");
        if (f.exists()) {
            f.delete();
        }

        //ezen a pontont a halozat mar teljesen fel van epitve
        //minden elem letezik es minden osszekottetes a helyen van
        //itt kell megcsinalni a topoorder
        //do the topoorder
        NetworkController NC = NetworkController.getInstance();
        NC.setGates(NC.DoTopoOrder(NC.getGates()));

        //to get everything to the starting point
        NC.resetNetwork();
    }

    public static void createConnection(int output, int outputPort, int input, int inputPort) throws Exception {
        //code
        //get the output element
        DigitalElement outElement = digitalElementList.get(Integer.valueOf(output));

        //get the input element, casting has to be valid
        //if it is not then the input value is wrong
        DigitalGate inGate = (DigitalGate) digitalElementList.get(Integer.valueOf(input));

        //create a wire to connect these
        Wire connector = new Wire(outElement, inGate, outputPort, inputPort);

        //initialize wire
        connector.setDigitalValue(Sign.Low);

        //make the connection
        outElement.addOutput(outputPort, connector);
        inGate.addInput(inputPort, connector);

    }

    public static DigitalElement getDigitalElement(int id) throws Exception {

        //letezik ez az id? ha igen akkor visszaadjuk a hozza tartozo elemet
        if (NetworkLoaderController.digitalElementList.containsKey(id) == false) {
            throw new Exception("# Invalid ID");
        }
        return digitalElementList.get(Integer.valueOf(id));
    }

    //needed for nested composits
    private static DigitalElement constructElement(ElementType type, int inputNum, int outputNum, String pattern) throws Exception {
        DigitalElement ret = null;

        //create the element
        switch (type) {
            case ANDGate:
                ret = new ANDGate(2, 1);
                break;
            case Composit:
                //pattern holds the recipe key
                ret = createCompositGate(pattern, inputNum, outputNum);
                break;
            case Generator:
                ret = new Generator(pattern, 1);
                break;
            case Inverter:
                ret = new Inverter(1, 1);
                break;
            case Led:
                ret = new Led(1, 0);
                break;
            case ORGate:
                ret = new ORGate(2, 1);
                break;
            case Switch:
                ret = new Switch(1);
                break;
            case Oscilloscope:
                ret = new Oscilloscope(1, 0);
                break;
        }
        return ret;
    }

    private static CompositGate createCompositGate(String recipe, int maxIn, int maxOut) throws Exception {
        //get the recipe
        CompositRecipe thisRec = compositRecipes.get(recipe);

        //create a compositegate
        CompositGate compGate = new CompositGate(maxIn, maxOut, thisRec.inMapping, thisRec.outMapping);

        //we have the skeleton now we need to fill it up
        //first create all the inside elements
        //get the ids
        Set<Integer> ids = thisRec.gateList.keySet();
        for (Integer id : ids) {
            //based on the element type, create the gate, and add it inside the composite element
            switch (thisRec.gateList.get(id)) {
                case ANDGate:
                    ANDGate gate = (ANDGate) constructElement(ElementType.ANDGate, 2, 1, null);
                    compGate.addGate(id, gate);
                    break;
                case Composit:
                    //we get the composite type from the extrainfo in the recipe
                    CompositGate comp = (CompositGate) constructElement(ElementType.Composit, maxIn, maxOut, thisRec.gateListExtraInfo.get(id));
                    compGate.addGate(id, compGate);
                    break;
                case Generator:
                    //we get the pattern from the extra info in the recipe
                    Generator gen = (Generator) constructElement(ElementType.Generator, 0, 1, thisRec.gateListExtraInfo.get(id));
                    compGate.addGenerator(id, gen);

                    //we need to add the generator to the nc as well so we can step it
                    NetworkController NC = NetworkController.getInstance();
                    NC.addGenerator(gen);
                    break;
                case Inverter:
                    Inverter inv = (Inverter) constructElement(ElementType.Inverter, 1, 0, null);
                    compGate.addGate(id, inv);
                    break;
                case ORGate:
                    ORGate org = (ORGate) constructElement(ElementType.ORGate, 2, 1, null);
                    compGate.addGate(id, org);
                    break;
            }
        }

        //now the elements exist inside the composit element
        //we need to make the connections
        for (Integer[] conn : thisRec.connections) {
            //get the output element
            DigitalElement outElement;
            if (compGate.generatorList.containsKey(conn[0])) {
                outElement = compGate.generatorList.get(conn[0]);
            } else {
                outElement = compGate.gateList.get(conn[0]);
            }

            //get the input element, casting has to be valid
            //if it is not then the input value is wrong
            DigitalGate inGate = (DigitalGate) compGate.gateList.get(conn[2]);

            //create a wire to connect these
            Wire connector = new Wire(outElement, inGate, 0, 0);

            //initialize wire
            connector.setDigitalValue(Sign.Low);

            //make the connection
            outElement.addOutput(conn[1], connector);
            inGate.addInput(conn[3], connector);
        }

        //now all connections are setup

        //we just need to give back the constructed gate
        return compGate;
    }

    public static void createDigitalElement(String id, ElementType type, String pattern) throws Exception {
        //get the NC instance
        NetworkController NC = NetworkController.getInstance();

        //create the element, add it to the NC, register it in the elementlist
        if (type.equals(ElementType.Switch)) {
            Switch sw = (Switch) constructElement(type, 0, 1, null);
            NC.addSwitch(sw);
            digitalElementList.put(Integer.parseInt(id), sw);
        } else if (type.equals(ElementType.Generator)) {
            Generator gen = (Generator) constructElement(type, 0, 1, pattern);
            NC.addGenerator(gen);
            digitalElementList.put(Integer.parseInt(id), gen);
        } else if (type.equals(ElementType.ANDGate)) {
            ANDGate gate = (ANDGate) constructElement(type, 2, 1, null);
            gate.addObserver(NC);
            NC.addGate(gate);
            digitalElementList.put(Integer.parseInt(id), gate);
        } else if (type.equals(ElementType.ORGate)) {
            ORGate gate = (ORGate) constructElement(type, 2, 1, null);
            gate.addObserver(NC);
            NC.addGate(gate);
            digitalElementList.put(Integer.parseInt(id), gate);
        } else if (type.equals(ElementType.Inverter)) {
            Inverter gate = (Inverter) constructElement(type, 1, 1, null);
            gate.addObserver(NC);
            NC.addGate(gate);
            digitalElementList.put(Integer.parseInt(id), gate);
        } else if (type.equals(ElementType.Led)) {
            Led gate = (Led) constructElement(type, 1, 0, null);
            gate.addObserver(NC);
            NC.addGate(gate);
            digitalElementList.put(Integer.parseInt(id), gate);
        } else if (type.equals(ElementType.Composit)) {
            CompositGate gate = (CompositGate) constructElement(type, 5, 5, pattern);
            NC.addGate(gate);
            digitalElementList.put(Integer.parseInt(id), gate);
        } else if (type.equals(ElementType.Oscilloscope)) {
            Oscilloscope osc = (Oscilloscope) constructElement(type, 1, 0, null);
            NC.addOscilloscope(osc);
            digitalElementList.put(Integer.parseInt(id), osc);
        }
    }
}
